/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.jseditor.client.popup;

import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventTarget;
import elemental.events.MouseEvent;
import elemental.html.ClientRect;
import elemental.html.Window;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.user.client.Timer;

import org.eclipse.che.ide.util.dom.Elements;

import static elemental.css.CSSStyleDeclaration.Unit.PX;

/**
 * Popup widow that hides itself on outside mouse down actions..
 */
public abstract class PopupWidget<T> {

    private static final int MIN_WIDTH = 400;
    private static final int MIN_HEIGHT = 130;

    protected final Element popupBodyElement;

    /** The list (ul) element for the popup. */
    private final Element listElement;

    private final EventListener popupListener;

    /**
     * The keyboard listener in the popup.
     */
    private final EventListener keyboardListener;

    protected final PopupResources popupResources;

    /**
     * The previously focused element.
     */
    private Element previousFocus;
    /** The main element for the popup. */
    private Element popupElement;

    public PopupWidget(final PopupResources popupResources, String title) {
        this.popupResources = popupResources;

        popupElement = Elements.createDivElement(popupResources.popupStyle().popup());

        Element headerElement = Elements.createDivElement(popupResources.popupStyle().header());
        headerElement.setInnerText(title);
        popupElement.appendChild(headerElement);

        popupBodyElement = Elements.createDivElement(popupResources.popupStyle().body());
        popupElement.appendChild(popupBodyElement);

        listElement = Elements.createUListElement();
        popupBodyElement.appendChild(listElement);

        popupListener = new EventListener() {
            @Override
            public void handleEvent(final Event evt) {
                if (evt instanceof MouseEvent) {
                    final MouseEvent mouseEvent = (MouseEvent) evt;
                    final EventTarget target = mouseEvent.getTarget();
                    if (target instanceof Element) {
                        final Element elementTarget = (Element) target;
                        if (!PopupWidget.this.popupElement.contains(elementTarget)) {
                            hide();
                            evt.preventDefault();
                        }
                    }
                }
                // else won't happen
            }
        };

        keyboardListener = new PopupKeyDownListener(this, this.listElement);
    }

    public abstract String getEmptyMessage();

    /** Create an element for the given item data. */
    public abstract Element createItem(final T itemModel);

    /**
     * Show the widget at the given document position.
     * @param left the horizontal pixel position in the document
     * @param top the vertical pixel position in the document
     */
    public void show(final float left, final float top) {
        if (!listElement.hasChildNodes()) {
            Element emptyElement = Elements.createLiElement(popupResources.popupStyle().item());
            emptyElement.setTextContent(getEmptyMessage());
            listElement.appendChild(emptyElement);
            return;
        }

        /* Reset popup dimensions and show. */
        popupElement.getStyle().setLeft(left, PX);
        popupElement.getStyle().setTop(top, PX);
        popupElement.getStyle().setWidth("" + MIN_WIDTH + "px");
        popupElement.getStyle().setHeight("" + MIN_HEIGHT + "px");
        popupElement.getStyle().setOpacity(0);
        Elements.getDocument().getBody().appendChild(popupElement);

        Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                popupElement.getStyle().setOpacity(1);
            }
        });

        Elements.getDocument().addEventListener(Event.MOUSEDOWN, popupListener, false);

        // does it fit inside the doc body?
        // This does exactly the same thing for height/top and width/left

        final Window window = Elements.getWindow();
        final int winX = window.getInnerWidth();
        final int winY = window.getInnerHeight();
        ClientRect widgetRect = this.popupElement.getBoundingClientRect();
        if (widgetRect.getBottom() > winY) {
            // it doesn't fit
            final float overflow = widgetRect.getBottom() - winY;
            if (widgetRect.getHeight() - overflow > MIN_HEIGHT) {
                // the widget can be shrunk to fit
                this.popupElement.getStyle().setHeight(widgetRect.getHeight() - overflow, PX);
            } else {
                // we need to shrink AND move the widget up
                this.popupElement.getStyle().setHeight(MIN_HEIGHT, PX);
                final int newTop = Math.max(winY - MIN_HEIGHT, MIN_HEIGHT);
                this.popupElement.getStyle().setTop(newTop, PX);
            }
        }
        // bounding rect has changed
        widgetRect = this.popupElement.getBoundingClientRect();
        if (widgetRect.getRight() > winX) {
            // it doesn't fit
            final float overflow = widgetRect.getRight() - winX;
            if (widgetRect.getWidth() - overflow > MIN_WIDTH) {
                // the widget can be shrunk to fit
                this.popupElement.getStyle().setWidth(widgetRect.getWidth() - overflow, PX);
            } else {
                // we need to shrink AND move the widget up
                this.popupElement.getStyle().setWidth(MIN_WIDTH, PX);
                final int newLeft = Math.max(winX - MIN_WIDTH, MIN_WIDTH);
                this.popupElement.getStyle().setLeft(newLeft - MIN_WIDTH, PX);
            }
        }

        if (needsFocus()) {
            // save previous focus and set focus in popup
            previousFocus = Elements.getDocument().getActiveElement();
            listElement.getFirstElementChild().focus();
        }

        // add key event listener on popup
        listElement.addEventListener(Event.KEYDOWN, keyboardListener, false);
    }

    /**
     * Add an item in the popup view.
     * @param itemModel the data for the item
     */
    public void addItem(final T itemModel) {
        if (itemModel == null) {
            return;
        }

        Element element = createItem(itemModel);
        element.setTabIndex(1);
        listElement.appendChild(element);
    }

    /** Hide the popup. */
    public void hide() {
        // restore previous focus state
        if (previousFocus != null) {
            previousFocus.focus();
            previousFocus = null;
        }

        popupElement.getStyle().setOpacity(0);
        new Timer() {
            @Override
            public void run() {
                if (popupElement != null) {
                    // detach assist popup
                    popupElement.getParentNode().removeChild(popupElement);
                    popupElement = null;
                }
                // remove all items from popup element
                listElement.setInnerHTML("");
            }
        }.schedule(250);

        // remove the keyboard listener
        listElement.removeEventListener(Event.KEYDOWN, keyboardListener, false);

        // remove the mouse listener
        Elements.getDocument().removeEventListener(Event.MOUSEDOWN, popupListener);
    }

    /**
     * Action taken when an item is validated.
     * @param itemElement the validated item
     */
    public void validateItem(final Element itemElement) {
        // by default, only hide the popup
        hide();
    }

    /**
     * Returns the widget viewed as an element.
     * @return the element
     */
    public Element asElement() {
        return this.popupElement;
    }

    /**
     * Tells if the popup widget wants focus.<br/>
     * Override the method to match needed value.
     * @return true iff the widget needs the focus
     */
    public boolean needsFocus() {
        return false;
    }
}
